This tutorial picks up at the end of JPA Tutorial 3 - A Mini Application. If you have not completed that tutorial, you can start with this source base:
Ignoring joints, a typical query essentially hits one table and returns back subset of the records in that single table. With JPA, we are not hitting tables but we are rather dealing with entities. What is the difference? An entity might map to a single table, multiple tables, or it could be involved in some kind of inheritance relationship.
What happens when we perform a query on an entity type that serves as a base class? It turns out that the actual "work" to make this happen is very simple. This tutorial along with the exercises gives you all the experience you'll need to figure out so-called polymorphic queries.
Introduction
So far, every query we've used has returned a single kind of result. In this tutorial we change things up a bit by moving from only using Books in our Library to working with Books and Dvd's, both of which inherit from Resource. When we search for a Resource we might get back Books, Dvd's, or both. This makes our queries polymorphic.
Introduce the Resource class and make the appropriate refactorings
Introduce a new type, Dvd
Setup
These instructions assume you are starting with the solutions mentioned above. If you finished JPA Tutorial 3 - A Mini Application, you have a few options:
Recommended Copy that project and follow the tutorial against the copy
Directly modify that project
Setup Basic Project
If you start with the jar file, here are the steps to get started:
In Eclipse, create a new Project: File:New:Project
Select Java Project and click Next
Enter a project name, e.g. JpaTutorial4, and click Finish
Open up the provided jar file and copy the contents into your project directory. For example, if your workspace directory is C:\workspaces\JpaAndEjb3\ and your project name is JpaTutorial4, then extract the contents of the jar file into C:\workspaces\JpaAndEjb3\JpaTutorial4\ making sure to overwrite .classpath and .project.
In Eclipse, select your project and refresh it (right-click, refresh)
Start Database Server
This set of source files assumes you've setup and started a Hypersonic server. The instructions to do so are here. Note that these instructions are both for configuring your database and configuring QuantumDb. If you are not using the QuantumDb plug-in, just pay attention to the sections on Start Your Database and JPA in JSE Settings.
Verify Your Project Works
Select your project in Eclipse
Right-click, select Run As:JUnit
All tests should pass. Please verify that the do before you continue.
V3 Requirements: Different Kinds of Resources
What happens when you perform a query on a type that has subclasses? That's the purpose of this tutorial's transformations. In JPA Tutorial 3 - A Mini Application, we assumed Patrons could only check out books. Now they can checkout Books or DVDs (once we've got two different kinds of resources, adding a third is not a big deal).
It turns out support for inheritance in queries (as well as JPA) is built in. In fact, you do not actually need to do anything other than have one entity inherit from another entity to get everything to work. There are three ways to represent inheritance:
One table for all classes in the hierarchy (the default)
One table for each concrete class
Table with subclasses (this is something that is optional to support in the current JPA spec.)
For now we'll stick with the default setting. Why? Which option you choose will impact performance, the database schema, how normalized your database is, but it will not affect how you write your code.
Step one we need to update our basic system. To do this we'll do the following:
Introduce a new entity type called Resource
Make the book entity inherit from the Resource entity
Move attributes and methods from book that apply to all resources up to the Resource class
Change the BookDao to be a ResourceDao
Re-introduce a stripped down BookDao to support searching by ISBN (which we'll assume apply to books but not DVD's)
Update all the methods that take books and replace them with resources (where appropriate)
Update the methods returning Book and have them instead return Resource (and List<Book> --> List<Resource>)
Update all references to Book and replace them with Resource
Update all the comments that talk about books to talk about resources
You get the idea, it's a lot of work to make this change. That's why we'll do this first and make sure all of our tests pass before we actually add a second kind of resource.
Note the source code for all of these changes is at the bottom of this page.
The Updated Entities
Resource.java
packageentity;importjava.util.Date;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToOne;importutil.DateTimeUtil;/**
* I represent the base of all things that can be checked of the Library. I hold
* the ID field for all kinds of resources.
*/
@Entitypublicabstractclass Resource {/**
* You do not (cannot in fact) add an id field to any subclasses since they
* already inherit it from me.
*/
@Id
@GeneratedValue
privateLong id;/**
* This was a book attribute, now it is moved up to Resource from Book (and
* remove from Book).
*/
@Column(length = 125, nullable = false)privateString title;/**
* This was a book attribute but since it makes sense for all resources, it
* is now here and inherited by all subclasses of resources.
*
* Now, instead of directly knowing the patron who has borrowed me as in the
* previous version, I now hold on to a Loan object. The loan tracks both
* me, the patron as well as the checkout and due dates.
*/
@OneToOne(mappedBy = "resource",
cascade = CascadeType.PERSIST, optional = true)private Loan loan;public Resource(){}public Resource(finalString title){this.title = title;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}public Loan getLoan(){return loan;}publicvoid setLoan(Loan loan){this.loan = loan;}publicString getTitle(){return title;}publicvoid setTitle(String title){this.title = title;}/**
* Each kind of resource has different rules for how long it can be checked
* out. This abstract method forces each kind of resource to specify its due
* date.
*
* @param checkoutDate
* The date from which to calculate the due date
*
* @return The date the resource is due back
*/publicabstractDate calculateDueDateFrom(finalDate checkoutDate);/**
* Calculate the fine for a given resource based on the number of days late
* the Patron returned it.
*
* @param daysLate
* Number of days this resource is late
*
* @return The fine
*/publicabstractdouble calculateFine(finalint daysLate);/**
* This was in Book but you check in all resources. Notice that this method
* does not know how to calculate the actual fine, so it delegates the fine
* calculation to the derived class. This method is an example of the
* Template Method Pattern.
*
* @param checkinDate
*/publicvoid checkin(finalDate checkinDate){finalDate dueDate = getDueDate();if(getLoan().getDueDate().before(checkinDate)){int daysLate = DateTimeUtil.daysBetween(dueDate, checkinDate);finaldouble fineAmount = calculateFine(daysLate);final Fine f = new Fine(fineAmount, checkinDate, this);
getLoan().getPatron().addFine(f);}
setLoan(null);}publicboolean isOnLoanTo(final Patron p){return(getLoan() == null&& p == null) || getLoan()!= null&& getLoan().getPatron().equals(p);}publicboolean isCheckedOut(){return getLoan()!= null;}publicboolean dueDateEquals(finalDate date){return(date == null&&!isCheckedOut())
|| getLoan().getDueDate().equals(date);}publicDate getDueDate(){if(isCheckedOut()){return getLoan().getDueDate();}returnnull;}}
It turns out that this change touched all of the entities. So here are the rest of the entities changed to reflect this new base class. Book.java
This class lost a lot of its methods as they were moved up to to Resource.
packageentity;importjava.util.Calendar;importjava.util.Date;importjava.util.HashSet;importjava.util.Set;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.ManyToMany;importjavax.persistence.NamedQueries;importjavax.persistence.NamedQuery;/**
* I represent a Book. I have one named query to find a book by its isbn number.
* I have a many to many relationship with author. Since I define the mappedBy,
* I'm the (arbitrarily picked) master of the relationship. I also take care of
* cascading changes to the database. I also have One To One relationship with a
* Loan that is optional (so it can be null). If I have a loan, I'm checked out
* and the loan knows the Patron, checkout date and due date.
*/
@Entity/**
* A named query must have a globally unique name. That is why these are named
* "Book."... These queries could be associated with any entity. Given that this
* query deals with books, it seems appropriate to put it here. Named queries
* will probably be pre-compiled. They are available from the entity manager by
* using em.getNamedQueyr("Book.findById").
*/
@NamedQueries({ @NamedQuery(name = "Book.findByIsbn",
query = "SELECT b FROM Book b WHERE b.isbn = :isbn")})publicclassBookextends Resource {
@Column(length = 20, nullable = false)privateString isbn;privateDate printDate;/**
* Authors may have written several books and vice-versa. We had to pick one
* side of this relationship as the primary one and we picked books. It was
* arbitrary but since we're dealing with books, we decided to make this
* side the primary size. The mappedBy connects this relationship to the one
* that is in Author. When we merge or persist, changes to this collection
* and the contents of the collection will be updated. That is, if we update
* the name of the author in the set, when we persist the book, the author
* will also get updated.
*
* Note that if we did not have the cascade setting here, they if we tried
* to persist a book with an unmanaged author (e.g. a newly created one),
* the entity manager would complain of a transient object.
*/
@ManyToMany(mappedBy = "booksWritten", cascade = { CascadeType.PERSIST,
CascadeType.MERGE})privateSet<Author> authors;publicBook(finalString t, finalString i, finalDate printDate,
final Author... authors){super(t);
setIsbn(i);
setPrintDate(printDate);for(Author a : authors){
addAuthor(a);}}publicBook(){}publicSet<Author> getAuthors(){if(authors == null){
authors = newHashSet<Author>();}return authors;}publicvoid setAuthors(finalSet<Author> authors){this.authors = authors;}publicString getIsbn(){return isbn;}publicvoid setIsbn(finalString isbn){this.isbn = isbn;}publicDate getPrintDate(){return printDate;}publicvoid setPrintDate(finalDate printDate){this.printDate = printDate;}publicvoid addAuthor(final Author author){
getAuthors().add(author);}
@Overridepublicboolean equals(finalObject rhs){return rhs instanceofBook&&((Book) rhs).getIsbn().equals(getIsbn());}
@Overridepublicint hashCode(){return getIsbn().hashCode();}publicboolean wasWrittenBy(Author a){return getAuthors().contains(a);}
@OverridepublicDate calculateDueDateFrom(Date checkoutDate){finalCalendar c = Calendar.getInstance();
c.setTime(checkoutDate);
c.add(Calendar.DATE, 14);return c.getTime();}
@Overridepublicdouble calculateFine(finalint daysLate){return .25 * daysLate;}}
Fine.java
We need to change all of Book references to instead be Resource.
packageentity;importjava.util.Date;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToOne;importjavax.persistence.Temporal;importjavax.persistence.TemporalType;/**
* I represent a single fine assigned to a Patron for a resource returned after its
* due date.
*
* I use new new features of JPA.
*/
@Entitypublicclass Fine {
@Id
@GeneratedValue
privateLong id;privatedouble amount;
@Temporal(TemporalType.DATE)privateDate dateAdded;
@OneToOne
private Resource resource;public Fine(){}public Fine(finaldouble amount, finalDate dateAdded,
final Resource resource){
setAmount(amount);
setDateAdded(dateAdded);
setResource(resource);}public Resource getResource(){return resource;}publicvoid setResource(final Resource resource){this.resource = resource;}publicDate getDateAdded(){return dateAdded;}publicvoid setDateAdded(Date dateAdded){this.dateAdded = dateAdded;}publicdouble getAmount(){return amount;}publicvoid setAmount(double fine){this.amount = fine;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}}
LoanId.java
Change the bookId to resourceId, update the names queries that refer to book to instead refer to Resource.
packageentity;importjava.io.Serializable;/**
* I'm a custom, multi-part key. I represent the key of a Loan object, which
* consists of a Patron id and a Resource id.
*
* These two values together must be unique. The names of my attributes are the
* same as the names used in Loan (patronId, rescoureId).
*
* I also must be Serializable.
*/publicclass LoanId implementsSerializable{privatestaticfinallong serialVersionUID = -2947379879626719748L;/**
* The following two fields must have names that match the names used in the
* Loan class.
*/privateLong patronId;privateLong resourceId;public LoanId(){}public LoanId(finalLong patronId, finalLong resourceId){this.patronId = patronId;this.resourceId = resourceId;}publicLong getResourceId(){return resourceId;}publicLong getPatronId(){return patronId;}
@Overridepublicboolean equals(finalObject rhs){return rhs instanceof LoanId
&&((LoanId) rhs).resourceId.equals(resourceId)&&((LoanId) rhs).patronId.equals(patronId);}
@Overridepublicint hashCode(){return patronId.hashCode()* resourceId.hashCode();}}
Loan.java
Loan refers to resource instead of book. This includes its join columns and named queries.
packageentity;importjava.util.Date;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.Id;importjavax.persistence.IdClass;importjavax.persistence.JoinColumn;importjavax.persistence.ManyToOne;importjavax.persistence.NamedQueries;importjavax.persistence.NamedQuery;importjavax.persistence.Temporal;importjavax.persistence.TemporalType;/**
* I'm an entity with a 2-part key. The first part of my key is a Resource id, the
* second is a Patron id.
* <P>
* To make this work, we must do several things: Use the annotation IdClass,
* Specify each of the parts of the id by using Id annotation (2 times in this
* case), Set each Id column to both insertable = false and updatable = false,
* Create an attribute representing the type of the id (Resource, Patron), Use
* JoinColumn with the name = id and insertable = false, updatable = false.
*/
@Entity
@IdClass(LoanId.class)
@NamedQueries({
@NamedQuery(name = "Loan.resourcesLoanedTo",
query = "SELECT l.resource FROM Loan l WHERE l.patron.id = :patronId"),
@NamedQuery(name = "Loan.byResourceId",
query = "SELECT l FROM Loan l WHERE l.resourceId = :resourceId"),
@NamedQuery(name = "Loan.overdueResources",
query = "SELECT l.resource FROM Loan l WHERE l.dueDate < :date"),
@NamedQuery(name = "Loan.patronsWithOverdueResources",
query = "SELECT l.patron FROM Loan l WHERE l.dueDate < :date")})publicclass Loan {/**
* Part 1 of a 2-part Key. The name must match the name in LoanId.
*/
@Id
@Column(name = "resourceId", insertable = false, updatable = false)privateLong resourceId;/**
* Part 2 of a 2-part Key. The name must match the name in LoanId.
*/
@Id
@Column(name = "patronId", insertable = false, updatable = false)privateLong patronId;/**
* A duplicate column in a sense, this one gives us the actual Patron rather
* than just having the id of the Patron.
*
* In the reference material I read, putting in insertable and updatable =
* false did not seem required. However, when using the hibernate entity
* manger I got a null pointer exception and had to step through the source
* code to fix the problem.
*/
@ManyToOne
@JoinColumn(name = "patronId", insertable = false, updatable = false)private Patron patron;/**
* Same comment as for patron attribute above.
*/
@ManyToOne
@JoinColumn(name = "resourceId", insertable = false, updatable = false)private Resource resource;/**
* The date type can represent a date, a time or a time stamp (date and
* time). In our case we just want the date.
*/
@Temporal(TemporalType.DATE)
@Column(updatable = false)privateDate checkoutDate;
@Temporal(TemporalType.DATE)
@Column(updatable = false)privateDate dueDate;public Loan(){}public Loan(final Resource r, final Patron p, finalDate checkoutDate){
setResourceId(r.getId());
setPatronId(p.getId());
setResource(r);
setPatron(p);
setCheckoutDate(checkoutDate);
setDueDate(r.calculateDueDateFrom(checkoutDate));}public Resource getResource(){return resource;}publicvoid setResource(final Resource resource){this.resource = resource;}publicLong getResourceId(){return resourceId;}publicvoid setResourceId(finalLong resourceId){this.resourceId = resourceId;}publicDate getCheckoutDate(){return checkoutDate;}publicvoid setCheckoutDate(finalDate checkoutDate){this.checkoutDate = checkoutDate;}publicDate getDueDate(){return dueDate;}publicvoid setDueDate(finalDate dueDate){this.dueDate = dueDate;}public Patron getPatron(){return patron;}publicvoid setPatron(final Patron patron){this.patron = patron;}publicLong getPatronId(){return patronId;}publicvoid setPatronId(finalLong patronId){this.patronId = patronId;}publicvoid checkin(finalDate checkinDate){
getResource().checkin(checkinDate);
getPatron().checkin(this);}}
Patron.java
The changes to Patron are a little less extreme. Other than some comments referring to books (you can find them), one method changed:
publicvoid checkout(final Resource r, finalDate checkoutDate){final Loan l = new Loan(r, this, checkoutDate);
getCheckedOutResources().add(l);
r.setLoan(l);}
The exceptions
Rename all of the exceptions with "Book" in their name. Replace "Book" with "Resource"
Rename all of the variables names bookId --> resourceId
How can you easliy go about doing this? Use the refactor factor in Eclipse. Select an exception class, right-click and select Refactor:Rename and enter a new name. You can also do the same thing by selecting the attribute name, right-click, refactor:rename and enter the new name. Make sure to select the bottom two selections regarding renaming the getter and setter.
The Dao's
BookDao.java
Most of the functionality that was in BookDao is now in ResourceDao.java. Why is this? Or better yet, why is the a BookDao at all? Look at the one method and answer the question for yourself (or ask).
Library.java
The Library has several changes. First, most references to Book have been replaced with Resource. Second, it now has 4 dao's instead of 3 (BookDao became ResourceDao and there's a new BookDao).
packagesession;importjava.util.Date;importjava.util.List;importjavax.persistence.EntityNotFoundException;importentity.Address;importentity.Author;importentity.Book;importentity.Loan;importentity.Patron;importentity.Resource;importexception.PatronHasFines;importexception.ResourceAlreadyCheckedOut;importexception.ResourceNotCheckedOut;/**
* This class provides a basic facade to the library system. If we had a user
* interface, it would interact with this object rather than dealing with all of
* the underlying Daos.
*/publicclass Library {private ResourceDao resourceDao;private BookDao bookDao;private PatronDao patronDao;private LoanDao loanDao;public ResourceDao getResourceDao(){return resourceDao;}publicvoid setResourceDao(final ResourceDao bookDao){this.resourceDao = bookDao;}public PatronDao getPatronDao(){return patronDao;}publicvoid setPatronDao(final PatronDao patronDao){this.patronDao = patronDao;}public LoanDao getLoanDao(){return loanDao;}publicvoid setLoanDao(final LoanDao loanDao){this.loanDao = loanDao;}public BookDao getBookDao(){return bookDao;}publicvoid setBookDao(BookDao bookDao){this.bookDao = bookDao;}publicBook createBook(finalString title, finalString isbn,
finalDate date, final Author a1, final Author a2){finalBook b = newBook(title, isbn, date, a1, a2);
getResourceDao().create(b);return b;}publicList<Book> findBookByIsbn(String isbn){return getBookDao().findByIsbn(isbn);}public Patron createPatron(finalString patronId, finalString fname,
finalString lname, finalString phoneNumber, final Address a){return getPatronDao().createPatron(fname, lname, phoneNumber, a);}public Patron findPatronById(finalLong id){final Patron p = getPatronDao().retrieve(id);if(p == null){thrownew EntityNotFoundException(String.format("Patron with id: %d does not exist", id));}return p;}public Resource findResourceById(Long id){final Resource r = getResourceDao().findById(id);if(r == null){thrownew EntityNotFoundException(String.format("Book with Id:%d does not exist", id));}return r;}publicvoid returnResource(finalDate checkinDate,
finalLong... resourceIds){for(Long resourceId : resourceIds){final Loan l = getLoanDao().getLoanFor(resourceId);if(l == null){thrownew ResourceNotCheckedOut(resourceId);}
l.checkin(checkinDate);
getLoanDao().remove(l);}}publicvoid checkout(finalLong patronId, finalDate checkoutDate,
finalLong... resourceIds){final Patron p = findPatronById(patronId);double totalFines = p.calculateTotalFines();if(totalFines > 0.0d){thrownew PatronHasFines(totalFines);}for(Long id : resourceIds){final Resource r = findResourceById(id);if(r.isCheckedOut()){thrownew ResourceAlreadyCheckedOut(id);}
p.checkout(r, checkoutDate);}}publicList<Resource> listResourcesOnLoanTo(finalLong patronId){return getLoanDao().listResourcesOnLoanTo(patronId);}publicList<Resource> findAllOverdueResources(finalDate compareDate){return getLoanDao().listAllOverdueResources(compareDate);}publicList<Patron> findAllPatronsWithOverdueBooks(finalDate compareDate){return getLoanDao().listAllPatronsWithOverdueResources(compareDate);}publicdouble calculateTotalFinesFor(finalLong patronId){return getPatronDao().retrieve(patronId).calculateTotalFines();}publicdouble tenderFine(finalLong patronId, double amountTendered){final Patron p = getPatronDao().retrieve(patronId);return p.pay(amountTendered);}}
LoanDao.java
This class no longer knows about books, it only knows about resources.
packagesession;importjava.util.Date;importjava.util.List;importjavax.persistence.NoResultException;importentity.Loan;importentity.Patron;importentity.Resource;/**
* Provide some basic queries focused around the Loan object. These queries
* could have been placed in either the PatronDao or ResourceDao. However,
* neither seemed like quite the right place so we created this new Dao.
*/publicclass LoanDao extends BaseDao {/**
* Given a resource id, find the associated loan or return null if none
* found.
*
* @param resrouceId
* Id of resource on loan
*
* @return Loan object that holds onto resourceId
*/public Loan getLoanFor(Long resourceId){try{return(Loan) getEm().createNamedQuery("Loan.byResourceId")
.setParameter("resourceId", resourceId).getSingleResult();}catch(NoResultException e){returnnull;}}publicvoid remove(final Loan l){
getEm().remove(l);}/**
* Return resources that are due after the compareDate.
*
* @param compareDate
* If a resource's due date is after compareDate, then it is
* included in the list. Note that this named query uses
* projection. Have a look at Loan.java.
*
* @return a list of all the resources that were due after this date.
*/
@SuppressWarnings("unchecked")publicList<Resource> listAllOverdueResources(finalDate compareDate){return getEm().createNamedQuery("Loan.overdueResources").setParameter("date", compareDate).getResultList();}/**
* Essentially the same query as listAllOverdueResources but we return the
* Patrons instead of the resources. This method uses a named query that
* uses projection.
*
* @param compareDate
* If a patron has at least one resources that was due after the
* compare date, include them.
*
* @return A list of the patrons with at least one overdue resources
*/
@SuppressWarnings("unchecked")publicList<Patron> listAllPatronsWithOverdueResources(finalDate compareDate){return getEm().createNamedQuery("Loan.patronsWithOverdueResources")
.setParameter("date", compareDate).getResultList();}/**
* Return all resources on loan to the provided patron id.
*
* @param patronId
* If patron id is invalid, this method will not notice it.
*
* @return Zero or more resources on loan to the patron in question
*/
@SuppressWarnings("unchecked")publicList<Resource> listResourcesOnLoanTo(finalLong patronId){return getEm().createNamedQuery("Loan.resourcesLoanedTo").setParameter("patronId", patronId).getResultList();}}
ResourceDao.java
Many of the BookDao functions are now here and they work with Resources intead of with Books (or rather they work with both but the interface deals with Resources).
packagesession;importentity.Resource;/**
* This class offers the basic create, read, update, delete functions required
* for a resource. As we implement more complex requirements, we'll be coming
* back to this class to add additional queries.
*/publicclass ResourceDao extends BaseDao {publicvoid create(final Resource r){
getEm().persist(r);}public Resource retrieve(finalLong id){return getEm().find(Resource.class, id);}publicvoid remove(Long id){final Resource r = retrieve(id);if(r != null){
getEm().remove(r);}}public Resource update(Resource r){return getEm().merge(r);}public Resource findById(Long id){return getEm().find(Resource.class, id);}}
The Tests
BookDaoTest.java
Removed (or moved to ResourceDaoTest, take your pick).
LibraryTest.java
Updated to work primarily with Resources instead of Books. Also, added additional initialization code since the library now has four dao's instead of 3.
packagesession;importstaticorg.junit.Assert.assertEquals;importstaticorg.junit.Assert.assertFalse;importstaticorg.junit.Assert.assertNotNull;importstaticorg.junit.Assert.assertTrue;importstaticorg.junit.Assert.fail;importjava.util.Calendar;importjava.util.Date;importjava.util.List;importjava.util.Set;importjavax.persistence.EntityNotFoundException;importorg.junit.Before;importorg.junit.BeforeClass;importorg.junit.Test;importutil.DateTimeUtil;importentity.Address;importentity.Author;importentity.Book;importentity.Name;importentity.Patron;importentity.Resource;importexception.InsufficientFunds;importexception.PatronHasFines;importexception.ResourceAlreadyCheckedOut;importexception.ResourceNotCheckedOut;publicclass LibraryTest extends EntityManagerBasedTest {privatestaticfinallong ID_DNE = -443123222l;privatestaticfinalString PATRON_ID = "113322";privatestaticfinalString ISBN = "1-932394-15-X";privatestaticDate CURRENT_DATE;privatestaticDate CURRENT_PLUS_8;privatestaticDate CURRENT_PLUS_14;privatestaticDate CURRENT_PLUS_15;private Library library;
@Before
publicvoid setupLibrary(){final ResourceDao rd = new ResourceDao();
rd.setEm(getEm());final PatronDao pd = new PatronDao();
pd.setEm(getEm());final LoanDao ld = new LoanDao();
ld.setEm(getEm());final BookDao bd = new BookDao();
library = new Library();
library.setResourceDao(rd);
library.setPatronDao(pd);
library.setLoanDao(ld);
library.setBookDao(bd);}
@BeforeClass
publicstaticvoid setupDates(){Calendar c = Calendar.getInstance();
DateTimeUtil.removeTimeFrom(c);
CURRENT_DATE = c.getTime();
c.add(Calendar.DAY_OF_MONTH, 8);
CURRENT_PLUS_8 = c.getTime();
c.add(Calendar.DAY_OF_MONTH, 6);
CURRENT_PLUS_14 = c.getTime();
c.add(Calendar.DAY_OF_MONTH, 1);
CURRENT_PLUS_15 = c.getTime();}
@Test
publicvoid addBook(){finalBook b = createBook();finalSet<Author> authors = b.getAuthors();final Resource found = library.findResourceById(b.getId());
assertTrue(found instanceofBook);
assertTrue(((Book) found).getAuthors().containsAll(authors));}
@Test(expected = EntityNotFoundException.class)publicvoid lookupBookThatDoesNotExist(){
library.findResourceById(ID_DNE);}
@Test
publicvoid addPatron(){final Patron p = createPatron();final Patron found = library.findPatronById(p.getId());
assertNotNull(found);}
@Test(expected = EntityNotFoundException.class)publicvoid lookupPatronThatDoesNotExist(){
library.findPatronById(ID_DNE);}
@Test
publicvoid checkoutBook(){finalBook b1 = createBook();finalBook b2 = createBook();final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, b1.getId(), b2.getId());finalList<Resource> list = library.listResourcesOnLoanTo(p.getId());
assertEquals(2, list.size());for(Resource r : list){
assertTrue(r.isOnLoanTo(p));
assertTrue(r.dueDateEquals(CURRENT_PLUS_14));}}
@Test
publicvoid returnBook(){finalBook b = createBook();final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, b.getId());finalint resourcesBefore = p.getCheckedOutResources().size();
assertTrue(b.isCheckedOut());
library.returnResource(CURRENT_PLUS_8, b.getId());
assertEquals(resourcesBefore - 1, p.getCheckedOutResources().size());
assertFalse(b.isCheckedOut());
assertEquals(0, p.getFines().size());}
@Test
publicvoid returnResourceLate(){finalBook b = createBook();final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, b.getId());
library.returnResource(CURRENT_PLUS_15, b.getId());
assertEquals(1, p.getFines().size());
assertEquals(.25, p.calculateTotalFines());}
@Test(expected = ResourceNotCheckedOut.class)publicvoid returnResourceThatsNotCheckedOut(){finalBook b = createBook();
assertFalse(b.isCheckedOut());
library.returnResource(CURRENT_PLUS_8, b.getId());}
@Test(expected = ResourceAlreadyCheckedOut.class)publicvoid checkoutBookThatIsAlreadyCheckedOut(){finalBook b = createBook();final Patron p1 = createPatron();final Patron p2 = createPatron();
library.checkout(p1.getId(), CURRENT_DATE, b.getId());
library.checkout(p2.getId(), CURRENT_DATE, b.getId());}
@Test(expected = EntityNotFoundException.class)publicvoid checkoutBookThatDoesNotExist(){final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, ID_DNE);}
@Test(expected = EntityNotFoundException.class)publicvoid checkoutBookToPatronThatDoesNotExist(){finalBook b = createBook();
library.checkout(ID_DNE, CURRENT_DATE, b.getId());}
@Test
publicvoid findOverdueBooks(){final Patron p = createPatron();finalBook b1 = createBook();finalBook b2 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.checkout(p.getId(), CURRENT_PLUS_8, b2.getId());finalList<Resource> notOverdue = library
.findAllOverdueResources(CURRENT_PLUS_8);
assertEquals(0, notOverdue.size());finalList<Resource> overdue = library
.findAllOverdueResources(CURRENT_PLUS_15);
assertEquals(1, overdue.size());
assertTrue(overdue.contains(b1));}
@Test
publicvoid patronsWithOverdueBooks(){final Patron p = createPatron();finalBook b1 = createBook();finalBook b2 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.checkout(p.getId(), CURRENT_PLUS_8, b2.getId());finalList<Patron> noPatrons = library
.findAllPatronsWithOverdueBooks(CURRENT_PLUS_14);
assertEquals(0, noPatrons.size());finalList<Patron> onePatron = library
.findAllPatronsWithOverdueBooks(CURRENT_PLUS_15);
assertEquals(1, onePatron.size());}
@Test
publicvoid calculateTotalFinesForPatron(){final Patron p = createPatron();finalBook b1 = createBook();finalBook b2 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.checkout(p.getId(), CURRENT_DATE, b2.getId());
library.returnResource(CURRENT_PLUS_15, b1.getId(), b2.getId());
assertEquals(.5, library.calculateTotalFinesFor(p.getId()));}
@Test
publicvoid payFineExactAmount(){final Patron p = createPatron();finalBook b1 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.returnResource(CURRENT_PLUS_15, b1.getId());double change = library.tenderFine(p.getId(), .25);
assertEquals(0d, change);
assertEquals(0, p.getFines().size());}
@Test(expected = InsufficientFunds.class)publicvoid payFineInsufficientFunds(){final Patron p = createPatron();finalBook b1 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.returnResource(CURRENT_PLUS_15, b1.getId());
library.tenderFine(p.getId(), .20);}/**
* This is an example of a test where we expect an exception. However,
* unlike other tests where we use expected=ExceptionClass.class, we need to
* catch the exception because we are additionally verifying a value in the
* thrown exception. This test is written how you'd write a test expecting
* an exception prior to JUnit 4.
*/
@Test
publicvoid patronCannotCheckoutWithFines(){final Patron p = createPatron();finalBook b1 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.returnResource(CURRENT_PLUS_15, b1.getId());finalBook b2 = createBook();try{
library.checkout(p.getId(), CURRENT_DATE, b2.getId());
fail(String.format("Should have thrown exception: %s",
PatronHasFines.class.getName()));}catch(PatronHasFines e){
assertEquals(.25, e.getTotalFines());}}privateBook createBook(){final Author a1 = new Author(newName("Christian", "Bauer"));final Author a2 = new Author(newName("Gavin", "King"));return library.createBook("Hibernate In Action", ISBN, Calendar
.getInstance().getTime(), a1, a2);}private Patron createPatron(){final Address a = new Address("5080 Spectrum Drive", "", "Dallas",
"TX", "75001");return library.createPatron(PATRON_ID, "Brett", "Schuchert",
"555-1212", a);}}
PatronDao.test
Only a comment changes in this one. If you use Eclipse's refactoring feature, it automatically gets updated.
ResourceDaoTest.java
The renamed and updated BookDaoTest.java. This class still builds books (they are currently the only kind of concrete subclass of entity).
packagesession;importstaticorg.junit.Assert.assertEquals;importstaticorg.junit.Assert.assertNotNull;importstaticorg.junit.Assert.assertNull;importstaticorg.junit.Assert.assertTrue;importjava.util.Calendar;importorg.junit.Test;importentity.Author;importentity.Book;importentity.Name;importentity.Resource;publicclass ResourceDaoTest extends BaseDbDaoTest {private ResourceDao dao;/**
* By overriding this method, I'm able to provide a dao to the base class,
* which then installs a new entity manager per test method execution. Note
* that my return type is not the same as the base class' version. I return
* BookDao whereas the base class returns BaseDao. Normally an overridden
* method must return the same type. However, it is OK for an overridden
* method to return a different type so long as that different type is a
* subclass of the type returned in the base class. This is called
* covariance.
*
* @see session.BaseDbDaoTest#getDao()
*/
@Overridepublic ResourceDao getDao(){if(dao == null){
dao = new ResourceDao();}return dao;}
@Test
publicvoid createABook(){finalBook b = createABookImpl();final Resource found = getDao().retrieve(b.getId());
assertNotNull(found);}privateBook createABookImpl(){final Author a1 = new Author(newName("Bill", "Burke"));final Author a2 = new Author(newName("Richard", "Monson-Haefel"));finalBook b = newBook("Enterprise JavaBeans 3.0",
"978-0-596-00978-6", Calendar.getInstance().getTime(), a1, a2);
getDao().create(b);return b;}
@Test
publicvoid removeABook(){finalBook b = createABookImpl();
Resource found = getDao().retrieve(b.getId());
assertNotNull(found);
getDao().remove(b.getId());
found = getDao().retrieve(b.getId());
assertNull(found);}
@Test
publicvoid updateABook(){finalBook b = createABookImpl();finalint initialAuthorCount = b.getAuthors().size();
b.addAuthor(new Author(newName("New", "Author")));
getDao().update(b);final Resource found = getDao().retrieve(b.getId());
assertTrue(found instanceofBook);
assertEquals(initialAuthorCount + 1, ((Book) found).getAuthors().size());}
@Test
publicvoid tryToFindBookThatDoesNotExist(){final Resource r = getDao().retrieve(-1123123123l);
assertNull(r);}}
Considering the amount of work required to get ready to add a resource, you might think that creating a hierarchy and then performing so-called polymorphic queries is difficult. It isn't. The difficulty of moving towards supporting a hierarchy is that we started with a concrete class and wanted to later introduce an abstract concept, the Resource.
In this final step for the tutorial, we'll create a second kind of resource, a DVD and then write some basic tests.
Supporting DVD's
The first basic test is creating a dvd and checking it out to a Patron. This is a test we add to LibraryTest.java.
First test: Checkout a Dvd
@Test
publicvoid checkoutDvd(){final Patron p = createPatron();final Dvd d = createDvd();
library.checkout(p.getId(), CURRENT_DATE, d.getId());
assertTrue(d.isOnLoanTo(p));
assertEquals(CURRENT_PLUS_6, d.getDueDate());}private Dvd createDvd(){return library.createDvd("Raiders of the Lost Ark", newName("Steven",
"Spielberg"));}
This test uses a new constant, CURRENT_PLUS_6, here's the change to LibraryTest to support that.
To get this to compile, we need to add support in the library class for creating a DVD. Here's that basic support:
Updating Library
public Dvd createDvd(finalString title, finalName directorsName){final Director director = new Director();
director.setName(directorsName);final Dvd d = new Dvd();
d.setTitle(title);
d.setDirector(director);
getResourceDao().create(d);return d;}
Notice that we have a director and a dvd in this example. Here are those classes: Director.java
packageentity;importjava.util.Calendar;importjava.util.Date;importjavax.persistence.CascadeType;importjavax.persistence.Entity;importjavax.persistence.ManyToOne;/**
* Notice that to get this to "work" all we need to do is inherit from Resource.
* As with book, we do not define an id field, since it is inherited.
*
* The way this class is represented in the database is determined by meta
* information in the Resource base class.
*
* Of course, regular Java rules apply here. The Resource base class defines 2
* abstract methods, both of which are overridden here.
*/
@Entitypublicclass Dvd extends Resource {
@ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST})private Director director;
@OverridepublicDate calculateDueDateFrom(finalDate checkoutDate){finalCalendar c = Calendar.getInstance();
c.setTime(checkoutDate);
c.add(Calendar.DATE, 6);return c.getTime();}
@Overridepublicdouble calculateFine(finalint daysLate){if(daysLate <5){return .3 * daysLate;}if(daysLate <10){return .6 * daysLate;}return6.0;}public Director getDirector(){return director;}publicvoid setDirector(Director director){this.director = director;}}
Resource.java
To get this to actually work, we need to use the @Inheritance annotation on resource. Here's the change:
As mentioned before, there are three kinds of representations for hierarchy. We are using the so-called JOINED approach, which means there is a table per class. It turns out that we need to use this approach based on how we've defined Book. Why that is will be answered in a following assignment.
A Few More Tests
Here are a few more tests to verify basic functionality. These also belong in LibraryTest.java.
@Test
publicvoid returnDvdLate(){final Dvd d = createDvd();final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, d.getId());
library.returnResource(CURRENT_PLUS_8, d.getId());
assertEquals(1, p.getFines().size());
assertEquals(0.6d, p.calculateTotalFines());}
@Test
publicvoid checkoutDvdAndBook(){final Dvd d = createDvd();finalBook b = createBook();final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, d.getId());
library.checkout(p.getId(), CURRENT_DATE, b.getId());finalList<Resource> list = library.listResourcesOnLoanTo(p.getId());
assertEquals(2, list.size());
assertTrue(d.isOnLoanTo(p));
assertTrue(b.isOnLoanTo(p));}
Summary: Inheritance
That's pretty much it. If we had started with a base class called Resource, this work would have been trivial. Adding new subclasses is quite easy. Figuring out which representation may take a little experimentation, but it's easy to change.
Queries are inherently polymorphic in nature if that makes sense for the given query. Your challenge is working with a base type insead of down-casting to a derived type.
V3 Assignments
Director <--> DVD
Right now the relationship between Director and DVD is one-way. Make it bidirectional.
Experiment with Inheritance Types
We use InheritanceType.JOINED. There are two others, SINGLE_TABLE, TABLE_PER_CLASS. Try out both of those and see if you can figure out what is happening for each of them. Be sure to re-run all the unit tests after switching to each type.
When you switch to the SINGLE_TABLE approach, what happens with column nullability, the isbn column (from Book), and the Dvd class?
When you switch to the TABLE_PER_CLASS approach, why does the key generation strategy have to change to in order fix the 'Cannot use identity column key generation' error? What did you change to, and why?
Can you sum up the advantages/disadvantages of each approach?
Sub-resource queries
Create a query that returns only Books or only Dvds.
Draw Tables
Create a visual representation of the database tables generated. Consider using SQuirreL SQL Client or the Quantum DB plugin for eclipse.
Update UI
Update your UI to allow a user to add a new DVD and a Patron to check out dvd's.
Name Equality
During the first offering of the class, one student asked about the Name object, which is used in an equals() and hashCode() method. It turns out that the unit tests written did not expose the fact that the Name class lacks these methosd.
Table of Contents
Inheritance and Polymorphic Queries
This tutorial picks up at the end of JPA Tutorial 3 - A Mini Application. If you have not completed that tutorial, you can start with this source base:Background
Ignoring joints, a typical query essentially hits one table and returns back subset of the records in that single table. With JPA, we are not hitting tables but we are rather dealing with entities. What is the difference? An entity might map to a single table, multiple tables, or it could be involved in some kind of inheritance relationship.What happens when we perform a query on an entity type that serves as a base class? It turns out that the actual "work" to make this happen is very simple. This tutorial along with the exercises gives you all the experience you'll need to figure out so-called polymorphic queries.
Introduction
So far, every query we've used has returned a single kind of result. In this tutorial we change things up a bit by moving from only using Books in our Library to working with Books and Dvd's, both of which inherit from Resource. When we search for a Resource we might get back Books, Dvd's, or both. This makes our queries polymorphic.We pick up at the end of JPA Tutorial 3 - A Mini Application and perform two major transformations:
Setup
These instructions assume you are starting with the solutions mentioned above. If you finished JPA Tutorial 3 - A Mini Application, you have a few options:Setup Basic Project
If you start with the jar file, here are the steps to get started:Start Database Server
This set of source files assumes you've setup and started a Hypersonic server. The instructions to do so are here. Note that these instructions are both for configuring your database and configuring QuantumDb. If you are not using the QuantumDb plug-in, just pay attention to the sections on Start Your Database and JPA in JSE Settings.Verify Your Project Works
All tests should pass. Please verify that the do before you continue.
V3 Requirements: Different Kinds of Resources
What happens when you perform a query on a type that has subclasses? That's the purpose of this tutorial's transformations. In JPA Tutorial 3 - A Mini Application, we assumed Patrons could only check out books. Now they can checkout Books or DVDs (once we've got two different kinds of resources, adding a third is not a big deal).It turns out support for inheritance in queries (as well as JPA) is built in. In fact, you do not actually need to do anything other than have one entity inherit from another entity to get everything to work. There are three ways to represent inheritance:
For now we'll stick with the default setting. Why? Which option you choose will impact performance, the database schema, how normalized your database is, but it will not affect how you write your code.
Step one we need to update our basic system. To do this we'll do the following:
You get the idea, it's a lot of work to make this change. That's why we'll do this first and make sure all of our tests pass before we actually add a second kind of resource.
Note the source code for all of these changes is at the bottom of this page.
The Updated Entities
Resource.javaIt turns out that this change touched all of the entities. So here are the rest of the entities changed to reflect this new base class.
Book.java
This class lost a lot of its methods as they were moved up to to Resource.
Fine.java
We need to change all of Book references to instead be Resource.
LoanId.java
Change the bookId to resourceId, update the names queries that refer to book to instead refer to Resource.
Loan.java
Loan refers to resource instead of book. This includes its join columns and named queries.
Patron.java
The changes to Patron are a little less extreme. Other than some comments referring to books (you can find them), one method changed:
The exceptions
How can you easliy go about doing this? Use the refactor factor in Eclipse. Select an exception class, right-click and select Refactor:Rename and enter a new name. You can also do the same thing by selecting the attribute name, right-click, refactor:rename and enter the new name. Make sure to select the bottom two selections regarding renaming the getter and setter.
The Dao's
BookDao.javaMost of the functionality that was in BookDao is now in ResourceDao.java. Why is this? Or better yet, why is the a BookDao at all? Look at the one method and answer the question for yourself (or ask).
Here's an updated version of BookDao:
Library.java
The Library has several changes. First, most references to Book have been replaced with Resource. Second, it now has 4 dao's instead of 3 (BookDao became ResourceDao and there's a new BookDao).
LoanDao.java
This class no longer knows about books, it only knows about resources.
ResourceDao.java
Many of the BookDao functions are now here and they work with Resources intead of with Books (or rather they work with both but the interface deals with Resources).
The Tests
BookDaoTest.java
Removed (or moved to ResourceDaoTest, take your pick).
LibraryTest.java
Updated to work primarily with Resources instead of Books. Also, added additional initialization code since the library now has four dao's instead of 3.
PatronDao.test
Only a comment changes in this one. If you use Eclipse's refactoring feature, it automatically gets updated.
ResourceDaoTest.java
The renamed and updated BookDaoTest.java. This class still builds books (they are currently the only kind of concrete subclass of entity).
The Source
V3 Adding a Second Kind of Resource
Considering the amount of work required to get ready to add a resource, you might think that creating a hierarchy and then performing so-called polymorphic queries is difficult. It isn't. The difficulty of moving towards supporting a hierarchy is that we started with a concrete class and wanted to later introduce an abstract concept, the Resource.In this final step for the tutorial, we'll create a second kind of resource, a DVD and then write some basic tests.
Supporting DVD's
The first basic test is creating a dvd and checking it out to a Patron. This is a test we add to LibraryTest.java.First test: Checkout a Dvd
@Test public void checkoutDvd() { final Patron p = createPatron(); final Dvd d = createDvd(); library.checkout(p.getId(), CURRENT_DATE, d.getId()); assertTrue(d.isOnLoanTo(p)); assertEquals(CURRENT_PLUS_6, d.getDueDate()); } private Dvd createDvd() { return library.createDvd("Raiders of the Lost Ark", new Name("Steven", "Spielberg")); }This test uses a new constant, CURRENT_PLUS_6, here's the change to LibraryTest to support that.To get this to compile, we need to add support in the library class for creating a DVD. Here's that basic support:
Updating Library
Notice that we have a director and a dvd in this example. Here are those classes:
Director.java
Dvd.java
Resource.java
To get this to actually work, we need to use the @Inheritance annotation on resource. Here's the change:
As mentioned before, there are three kinds of representations for hierarchy. We are using the so-called JOINED approach, which means there is a table per class. It turns out that we need to use this approach based on how we've defined Book. Why that is will be answered in a following assignment.
A Few More Tests
Here are a few more tests to verify basic functionality. These also belong in LibraryTest.java.
@Test public void returnDvdLate() { final Dvd d = createDvd(); final Patron p = createPatron(); library.checkout(p.getId(), CURRENT_DATE, d.getId()); library.returnResource(CURRENT_PLUS_8, d.getId()); assertEquals(1, p.getFines().size()); assertEquals(0.6d, p.calculateTotalFines()); } @Test public void checkoutDvdAndBook() { final Dvd d = createDvd(); final Book b = createBook(); final Patron p = createPatron(); library.checkout(p.getId(), CURRENT_DATE, d.getId()); library.checkout(p.getId(), CURRENT_DATE, b.getId()); final List<Resource> list = library.listResourcesOnLoanTo(p.getId()); assertEquals(2, list.size()); assertTrue(d.isOnLoanTo(p)); assertTrue(b.isOnLoanTo(p)); }Summary: Inheritance
That's pretty much it. If we had started with a base class called Resource, this work would have been trivial. Adding new subclasses is quite easy. Figuring out which representation may take a little experimentation, but it's easy to change.Queries are inherently polymorphic in nature if that makes sense for the given query. Your challenge is working with a base type insead of down-casting to a derived type.
V3 Assignments
Director <--> DVD
Right now the relationship between Director and DVD is one-way. Make it bidirectional.Experiment with Inheritance Types
We use InheritanceType.JOINED. There are two others, SINGLE_TABLE, TABLE_PER_CLASS. Try out both of those and see if you can figure out what is happening for each of them. Be sure to re-run all the unit tests after switching to each type.Sub-resource queries
Create a query that returns only Books or only Dvds.Draw Tables
Create a visual representation of the database tables generated. Consider using SQuirreL SQL Client or the Quantum DB plugin for eclipse.Update UI
Update your UI to allow a user to add a new DVD and a Patron to check out dvd's.Name Equality
During the first offering of the class, one student asked about the Name object, which is used in an equals() and hashCode() method. It turns out that the unit tests written did not expose the fact that the Name class lacks these methosd.Here's a unit test, add it and get it to work:
Individual Links
JPA Tutorial 4 - IntroductionJPA Tutorial 4 - Setup
JPA Tutorial 4 - Different Kinds of Resources
JPA Tutorial 4 - Adding a Second Kind of Resource
JPA Tutorial 4 - Assignments
<--Back